/*******************************************************************

  CPSC453 LEC02 TUT07 - ASSIGNMENT 3
  By John R. Zhang
  April 18, 2006.

  Adapted for Pigasus Demo.

  ==================================================================

  Files required:
	- raytrace.cc

  To run:
	$> g++ -lm raytrace.cc
	$> ./a.out <output file name> <x-coordinate for red sphere> &

  This is the raytracer and outputs to a ppm file.

  For more details on the implementation and use, please see the 
  readme.

 *******************************************************************/

#include <math.h>
#include <stdlib.h>
#include <stdio.h>

///////////////////////   PREDEFINES   ///////////////////////

#define HEIGHT 			600		// height of image
#define WIDTH			800		// width of image
#define SCREEN_Z		0		// virutal of screen z-coordinate

#define WORLD_H			6.0		// virtual units height
#define WORLD_W			8.0		// virtual units wide
#define WORLD_D			8.0		// virtual units deep

#define EPS				0.001
#define TRACESTEPS		6
#define PI				3.141592653589793238462
#define MAXDIST			10000.0

#define MIN(A,B)		((A<B)?A:B)
#define MAX(A,B)		((A<B)?B:A)
#define SQR(A)			((A)*(A))

#define DOT(A,B)		(A.x*B.x+A.y*B.y+A.z*B.z)
#define SQRLENGTH(A)	(SQR(A.x)+SQR(A.y)+SQR(A.z))
#define LENGTH(A)		(sqrt(SQRLENGTH(A)))
#define SQRDIST(A,B)	(SQR(A.x-B.x)+SQR(A.y-B.y)+SQR(A.z-B.z))
#define DIST(A,B)		(sqrt(SQRDIST(A,B)))
#define NORMALIZE(A)	{double l=1.0/LENGTH(A);A.x*=l;A.y*=l;A.z*=l;}

///////////////////////   STRUCTURES   ///////////////////////

typedef struct {
	double x, y, z;
} Point;

typedef Point Vect;

typedef struct {
	Point origin;
	Vect dir;
} Ray;

typedef struct {
	double r, g, b;
} Color;

typedef struct {
	Color color;
	double refl;
	double diff;
	double specular;
} Material;

typedef struct {
	Point center;
	double radius;
	Material mat;
} Sphere;


///////////////////////   GLOBALS   ///////////////////////

Point eye;				// camera
Point light;			// light position
Color lightColor;		// light color
Sphere spheres[5];		// list of spheres
int nspheres;			// number of spheres

///////////////////////   BASIC 3D MATH   ///////////////////////

void add(Vect *res, Vect *a, Vect *b) {
	res->x = a->x + b->x;
	res->y = a->y + b->y;
	res->z = a->z + b->z;
}

void sub(Vect *res, Vect *a, Vect *b) {
	res->x = a->x - b->x;
	res->y = a->y - b->y;
	res->z = a->z - b->z;
}

///////////////////////   EDIT COLOR   ///////////////////////

void setColor(Color *c, double nr, double ng, double nb) {
	c->r = nr;
	c->g = ng;
	c->b = nb;
}

///////////////////////   EDIT 3D OBJ   ///////////////////////

void set3d(Point *p, double nx, double ny, double nz) {
	p->x = nx;
	p->y = ny;
	p->z = nz;
}	

///////////////////////   INTERSECTION   ///////////////////////

int intersect(Sphere *s, Ray *r, double *ad) {	
	Vect v;
	sub(&v, &(r->origin), &(s->center));
	double b = -1*DOT(v, r->dir);
	double det = SQR(b) - DOT(v,v) + SQR(s->radius);
	
	int ret = 0;
	if (det > 0) {
		det = sqrt(det);
		double i1 = b - det;
		double i2 = b + det;
		if (i2 > 0) {
			if (i1 < 0) {
				if (i2 < *ad) {
					*ad = i2;
					ret = 1;
				}
			} else {
				if (i1 < *ad) {
					*ad = i1;
					ret = 2;
				}
			}
		}
	}
	return ret;
}

///////////////////////   GET NORMAL   ///////////////////////

void getNorm(Vect *n, Vect *a, Vect *c, double r) {
	sub(n, a, c);
	n->x *= r;
	n->y *= r;
	n->z *= r;		
}

///////////////////////   RAYTRACER   ///////////////////////

void raytrace(Ray *ray, Color *pixel, int depth, double *aDist ) {
	if (depth > TRACESTEPS) return;
	
	int ss=-1, result;
	double shade = 1.0;
	
	// check for intersection along all objects
	for (int k=0; k<nspheres; k++) {
		if (int res = intersect(&spheres[k], ray, aDist)) {
			ss = k;
			result = res;
		}
	}		
			
	if ((!result) || (ss == -1))
		return;
	
	Vect pi;
	pi.x = ray->origin.x + ray->dir.x * (*aDist);
	pi.y = ray->origin.y + ray->dir.y * (*aDist);
	pi.z = ray->origin.z + ray->dir.z * (*aDist);
							
	/** do lighting **/
	Vect ll;					
	sub(&ll, &light, &pi);					
	NORMALIZE(ll);					
	Vect norm;				
	
	norm.x = (pi.x - spheres[ss].center.x)/spheres[ss].radius;
	norm.y = (pi.y - spheres[ss].center.y)/spheres[ss].radius;
	norm.z = (pi.z - spheres[ss].center.z)/spheres[ss].radius;
	
	
	// reflection
	if (spheres[ss].mat.refl > 0) {
		Vect r;
		r.x = ray->dir.x - 2.0*DOT(ray->dir, norm) * norm.x;
		r.y = ray->dir.y - 2.0*DOT(ray->dir, norm) * norm.y;
		r.z = ray->dir.z - 2.0*DOT(ray->dir, norm) * norm.z;
		
		if (depth < TRACESTEPS) {
			Color t;			
			double d = MAXDIST;
			Ray refl_ray;
			Vect no;
			
			t.r = t.g = t.b = 0.0;
			no.x = pi.x + r.x * EPS;
			no.y = pi.y + r.y * EPS;
			no.z = pi.z + r.z * EPS;
			
			refl_ray.origin.x = no.x;
			refl_ray.origin.y = no.y;
			refl_ray.origin.z = no.z;
			
			refl_ray.dir.x = r.x;
			refl_ray.dir.y = r.y;
			refl_ray.dir.z = r.z;
			
			raytrace(&refl_ray, &t, depth+1, &d);
			
			pixel->r += t.r;			
			pixel->g += t.g;
			pixel->b += t.b;
		}		
	}	
	
	// shadows
	Vect lls;
	lls.x = ll.x;
	lls.y = ll.y;
	lls.z = ll.z;
	
	double tdist = LENGTH(lls);
	lls.x *= 1.0/tdist;
	lls.y *= 1.0/ tdist;
	lls.z *= 1.0/ tdist;
	
	Ray ro;
	ro.origin.x = pi.x + lls.x * EPS;
	ro.origin.y = pi.y + lls.y * EPS;
	ro.origin.z = pi.z + lls.z * EPS;
	
	ro.dir.x = lls.x;
	ro.dir.y = lls.y;
	ro.dir.z = lls.z;
	
	NORMALIZE(ro.dir);
	
	Sphere ls;
	set3d(&ls.center, light.x, light.y, light.z);
	ls.radius = 1.0;
		
	if (intersect(&ls, &ro, &tdist )) {
		// printf("shade");
		shade = 0.0;		 
	}
	
	// diffuse light
	if (spheres[ss].mat.diff > 0) {
		double dot = DOT(norm, ll);							
		if (dot > 0) {
			double dif = dot * spheres[ss].mat.diff * shade;
			pixel->r += dif * spheres[ss].mat.color.r * lightColor.r;
			pixel->g += dif * spheres[ss].mat.color.g * lightColor.g;
			pixel->b += dif * spheres[ss].mat.color.b * lightColor.b;
		}
	}
	
	// specular
	Vect sv;
	Vect sr;
	
	sv.x = ray->dir.x;
	sv.y = ray->dir.y;
	sv.z = ray->dir.z;
	
	sr.x = ll.x -2.0*DOT(ll, norm) * norm.x;
	sr.y = ll.y -2.0*DOT(ll, norm) * norm.y;
	sr.z = ll.z -2.0*DOT(ll, norm) * norm.z;
	
	double sdot = DOT(sv, sr);
	if (sdot > 0) {
		double spec = pow(sdot, 20) * spheres[ss].mat.specular * shade;
		pixel->r += spec * lightColor.r;
		pixel->g += spec * lightColor.g;
		pixel->b += spec * lightColor.b;
	}	
	
}

///////////////////////   INIT OBJECTS   ///////////////////////

void init(double sphere_x) {
	set3d(&eye, 0, 0, -50);		// set eye position
	set3d(&light, -3, -20, -100);	// set light position
	
	lightColor.r = lightColor.g = lightColor.b = 1.0;	// white light
	
	nspheres = 0;
	
	// add a sphere
	set3d(&spheres[nspheres].center, sphere_x, 0, 1);
	setColor(&spheres[nspheres].mat.color, 1.0, 0.0, 0.0);
	spheres[nspheres].radius = 1.0;
	spheres[nspheres].mat.refl = 0.1;
	spheres[nspheres].mat.diff = 0.8;
	spheres[nspheres].mat.specular = 0.5;
	
	nspheres++;	
	
	// add a sphere
	set3d(&spheres[nspheres].center, -2, -2, 1);
	setColor(&spheres[nspheres].mat.color, 0.0, 1.0, 0.0);
	spheres[nspheres].radius = 0.5;
	spheres[nspheres].mat.refl = 0.6;
	spheres[nspheres].mat.diff = 0.2;
	spheres[nspheres].mat.specular = 0.5;
	
	nspheres++;	

	// add a sphere
	set3d(&spheres[nspheres].center, 1, 2, -5);
	setColor(&spheres[nspheres].mat.color, 0.0, 0.0, 1.0);
	spheres[nspheres].radius = 0.75;
	spheres[nspheres].mat.refl = 0.6;
	spheres[nspheres].mat.diff = 0.8;
	spheres[nspheres].mat.specular = 0.9;
	
	nspheres++;
	/*
  // add a sphere
	set3d(&spheres[nspheres].center, 0, 8, 10);
	setColor(&spheres[nspheres].mat.color, 0.6, 0.6, 0.8);
	spheres[nspheres].radius = 2;
	spheres[nspheres].mat.refl = 0.0;
	spheres[nspheres].mat.diff = 0.8;
	spheres[nspheres].mat.specular = 1.0;
	
	nspheres++;	
	*/
}


///////////////////////   MAIN   ///////////////////////

int main(int argc, char **argv) {
	
	init(atof(argv[2]));
	
	/* do tracing and output ppm file (use eog to view) */			
	FILE *outfile = fopen(argv[1], "w");
	if (outfile == NULL) {
		printf("Error with file %s.\n", argv[1]);
		return 0;
	}
	
	fprintf(outfile, "P3\n#Author: John R. Zhang\n%d %d\n\0", WIDTH, HEIGHT);
	fprintf(outfile, "255\n");									
	for (int i=0; i<HEIGHT; i++) {
		for (int j=0; j<WIDTH; j++) {
			// convert pixels to virtual coordinates
			double v_h = (double)(i-HEIGHT/2) / 100.0;
			double v_w = (double)(j-WIDTH/2) / 100.0;
			double v_d = (double)SCREEN_Z;						
			double aDist = MAXDIST;		
			Color pixel;
			
			pixel.r = pixel.g = pixel.b = 0.0;	
			
			Ray ray;
			set3d(&ray.origin, eye.x, eye.y, eye.z);
			set3d(&ray.dir, v_w-eye.x, v_h-eye.y, v_d-eye.z);	// set direction
			
			NORMALIZE(ray.dir);
			
			raytrace(&ray, &pixel, 0, &aDist);
			
			if (pixel.r > 1.0) pixel.r = 1.0;
			if (pixel.g > 1.0) pixel.g = 1.0;
			if (pixel.b > 1.0) pixel.b = 1.0;
			
			fprintf(outfile, "%d %d %d\n\0", (int)(pixel.r * 255.0), (int)(pixel.g * 255.0), (int)(pixel.b * 255.0));
		}
	}	
	fclose(outfile);
	
	return 0;
}
